//Compiler for translating xml dom to code
"use strict";

import { HtmlParser } from './HtmlParser.js'

function isTextExp(text) {
    isTextExp.re = /^\s*{{(.*)}}\s*$/;
    if (!text) {
        return null;
    }
    var m = text.match(isTextExp.re);
    if (!m) {
        return null;
    }
    return m[1];
}

function cloneAttrs(attrs) {
    if (!attrs) {
        return {};
    }
    var clone = {};
    if (Array.isArray(attrs)) {
        attrs.forEach(function (attr) {
            clone[attr.name] = attr.value
        });
    }
    else {
        Object.keys(attrs).forEach(function (key) {
            clone[key] = attrs[value];
        });
    }
    return clone;
}

//----- Node -----
function Node(type, tag, attrs, text) {
    this.type = type;
    this.tag = tag || null;
    this.attrs = attrs || [];
    this.unhandleAttrs = cloneAttrs(this.attrs);

    this.parent = null;
    this.children = [];

    // text node only
    this.text = text || null;
    this.exp = text ? isTextExp(text) : null;

    // event handler binding
    this.events = {};

    // directive info
    this.if = null;
    this.ifProcessed = false;
    this.elseif = null;
    this.else = null;
    this.ifConditions = null;
}

Node.Type = {
    None: 0,
    Element: 1,
    Text: 3,
    Comment: 8,

};

Node.prototype.addChild = function (node) {
    node.parent = this.parent;
    this.children.push(node);
};

Node.prototype.getPrevChild = function (node) {
    var nodeIndex = this.children.findIndex(function (child) {
        if (child === node) {
            return true;
        }
        return false;
    });
    nodeIndex -= 1;
    if (nodeIndex >= 0) {
        return this.children[nodeIndex];
    }
    return null;
};

Node.prototype.removeChild = function (node) {
    var index = this.children.findIndex(function (v) {
        return v === node;
    });
    if (index < 0) {
        return false;
    }
    this.children.splice(index, 1);
    return true;
}

Node.prototype.getAndRemoveUnhandleAttr = function (key) {

    if (typeof key === 'string') {
        if (key in this.unhandleAttrs) {
            let v = this.unhandleAttrs[key];
            delete this.unhandleAttrs[key];
            return v;
        }
    }
    else if (typeof key === 'function') {
        let keys = Object.keys(this.unhandleAttrs);
        let i = keys.findIndex(key);
        if (i >= 0) {
            key = keys[i];
            let v = this.unhandleAttrs[key];
            delete this.unhandleAttrs[key];
            return v;
        }
    }
    return null;
};

Node.prototype.getChild = function (index) {
    return this.children[index];
};

Node.prototype.getChildCount = function () {
    return this.children.length;
};


//----- Compiler -----
function addIfCondition(node, condition) {
    if (node.ifConditions === null) {
        node.ifConditions = [];
    }
    node.ifConditions.push(condition);
}

function handleIf(node) {
    var exp;
    if ((exp = isTextExp(node.getAndRemoveUnhandleAttr('wx:if')))) {
        node.if = exp;
        addIfCondition(node, {exp: exp, block: node});
    }
    else if ((exp = isTextExp(node.getAndRemoveUnhandleAttr('wx:elif')))) {
        node.elseif = exp;
    }
    else if (node.getAndRemoveUnhandleAttr('wx:else') !== null) {
        node.else = true;
    }
}

function handleFor(node) {

    handleFor.re_forin = /^\s*([\w$_]+)\s+in\s+([\w$_]+)$/;
    handleFor.re_id = /^\s*([\w$_]+)\s*/;

    var text = node.getAndRemoveUnhandleAttr('wx:for');
    var exp = isTextExp(text);
    if (exp) {
        var match;
        if ((match = exp.match(handleFor.re_id))) { //enum by index
            node.for = exp;
            node.forVar = "item";   //default var for iterator
            node.forArray = match[1];
        }
        else if ((match = exp.match(handleFor.re_forin))) { //enum by forEach
            node.for = exp;
            node.forVar = match[1];
            node.forArray = match[2];
        }
    }
}

function handleEventBinding(node) {

    const prefixList = ['bind:', 'bind', 'catch:', 'catch'];
    const eventNameList = ["tap", "click", 'touchstart', 'touchmove', 'touchend', 'touchcancel'];

    function isValidEvent(prop) {
        return prefixList.some((prefix)=>{
            if (!prop.startsWith(prefix)) {
                return false;
            }
            prop = prop.substring(prefix.length);
            return eventNameList.some((name)=>{
                return prop === name;
            });
        });
    }

    while (true)
    {
        let eventName = null;
        let exp = node.getAndRemoveUnhandleAttr((key) => {
            if (isValidEvent(key)) {
                eventName = key;
                return true;
            }
            return false;
        });

        if (!exp) {
            break;
        }

        if (exp.match(reIdentifier)) {
            //the framework need pass binding function name to the logic thread,
            //in this case, we need a string type instead of a function type
            node.events[eventName] = exp;
        }
        else {
            node.events[eventName] = exp;
        }
    }
}

function handleOtherAttrs(node) {

}

function adjustPrevNodeIfCondition(node, parent) {
    var prevNode = parent.getPrevChild(node);
    if (prevNode && prevNode.if) {
        addIfCondition(prevNode, {exp: node.elseif, block: node});
        //remove from parent node to avoid multiple parsing while generating code
        parent.removeChild(node);
    }
    else {
        console.warn("else directive without if directive");
    }
}


var createNodeTreeFromXML = function (xml) {

    var root = null;
    var nodeStack = [];

    function getTopNode() {
        return nodeStack.length === 0 ? null : nodeStack[nodeStack.length - 1];
    }

    var parserOption = {
        startElement: function (tag, attrs) {

            var node = new Node(Node.Type.Element, tag, attrs);

            handleIf(node);
            handleFor(node);
            handleEventBinding(node);
            handleOtherAttrs(node);

            var parent = getTopNode();
            if (!parent) {
                if (root) {
                    throw new Error("multiple top level node found.");
                }
                root = node;
            }
            else {
                parent.addChild(node);
            }
            nodeStack.push(node);

            //handle if directive
            if (parent) {
                if (node.elseif || node.else) {
                    adjustPrevNodeIfCondition(node, parent);
                }
            }
        },
        endElement: function (tag) {
            nodeStack.pop();
        },
        characters: function (text) {
            var node = new Node(Node.Type.Text, null, null, text);
            var parent = getTopNode();
            parent.addChild(node);
        }
    };

    var parser = new HtmlParser(parserOption);
    try {
        parser.parse(xml);
        return root;
    }
    catch (e) {
        console.warn("createVDOM fail, msg: " + e.message);
        throw e;
    }
};

//----------

function genAttrs(attrs) {
    var s = "";
    Object.keys(attrs).forEach(function (key) {

        var value = attrs[key];
        var exp = isTextExp(value);

        s += '"' + key + '":';
        if (exp) {
            s += '(' + exp + ')';
        }
        else {
            s += JSON.stringify(value);
        }
        s += ','
    });
    return s.slice(0, -1);
}

const reFunctionDefine = /^\s*([\w$_]+|\([^)]*?\))\s*=>|^function\s*\(/;
const reSimplePath = /^\s*[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?']|\[".*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*\s*$/;
const reIdentifier = /^\s*[A-Za-z_$][\w$]*\s*$/;

function genEvents(events) {
    let code = "";
    Object.keys(events).forEach(function(eventName) {

        var exp = events[eventName];
        if (exp.match(reFunctionDefine)) {   //function definition
            code += eventName + ":" + exp + ",";
        }
        else if (exp.match(reSimplePath)) { //function name
            code += eventName + ":" + JSON.stringify(exp.trim()) + ",";
        }
        else {  //expression
            code += eventName + ":" + "function($event){" + exp + "},"
        }
    });
    return code.slice(0, -1);
}

function genNodeData(node) {
    var data = "{";

    if (node.events) {
        data += 'events:{' + genEvents(node.events) + "},";
    }

    if (node.unhandleAttrs) {
        data += 'attrs:{' + genAttrs(node.unhandleAttrs) + '},';
    }

    data = data.replace(/,$/, '') + "}";
    return data;
}

function genIfConditions(conditions) {
    if (conditions.length === 0) {
        return "_createEmptyNode()";
    }

    var condition = conditions.shift();
    if (condition.exp) {
        var code = "(" + condition.exp + ")?"
            + "(" + genCodeForElementNode(condition.block) + ")"
            + ":(" + genIfConditions(conditions) + ")";
        return code;
    }
    else {
        return genCodeForElementNode(condition.block);
    }
}

function genCodeIfDirective(node) {
    //if directive can be considered as a virtual parent node which its children is all if, elif, else nodes
    //mark ifProcessed as true, and we will process it like it's one the children of 'if' virtual node
    node.ifProcessed = true;
    return genIfConditions(node.ifConditions.slice());
}


/*
    _clist(array, function(item) {
        return _c('tag', attrs, [child1, child2, ...]);
    ));
*/
function genCodeForDirective(node) {
    node.forProcessed = true;

    var nodeCode = genCodeForNode(node);

    var code = "_clist("
        + "(" + node.forArray + "),"
        + "function(" + node.forVar + "){"
        + "return (" + nodeCode + ");"
        + "})";

    return code;
}


function genCodeForTextNode(node) {

    var exp = isTextExp(node.text);
    if (exp) {
        //exp = "_expr('" + exp + "')";
        exp = "(" + exp + ")";
    }
    else {
        exp = JSON.stringify(node.text);
    }

    var code = "_createTextNode(" + exp + ")";
    return code;
}

function genCodeForElementNode(node) {
    if (node.for && !node.forProcessed) {
        return genCodeForDirective(node);
    }
    else if (node.if && !node.ifProcessed) {
        return genCodeIfDirective(node);
    }
    else {
        var data = genNodeData(node);
        var children = genNodeChildren(node);
        var code = "\n_createElementNode('" + node.tag + "'"
            + "," + (data ? data : '{}')
            + "\n," + (children ? children : '[]')
            + ")";

        return code;
    }
}

function genCodeForNode(node) {
    if (node.type === Node.Type.Text) {
        return genCodeForTextNode(node);
    }
    else if (node.type === Node.Type.Element) {
        return genCodeForElementNode(node);
    }
    else {
        console.assert(false, "invalid node type '" + node.type + "'")
        return "";
    }
}

function genNodeChildren(node) {
    var childCount = node.getChildCount();

    var code = "[";
    for (var i = 0; i < childCount; ++i) {
        var c = genCodeForNode(node.getChild(i));

        if (i > 0) {
            code += ",";
        }
        code += "(" + c + ")";
    }
    code += "]";
    return code;
}

function genCodeForXMLTemplate(xml)
{
    try {
        var node = createNodeTreeFromXML(xml);
        var code = genCodeForNode(node);
        return code;
    }
    catch (e) {
        console.warn("genCodeForXMLTemplate, exception: " + e.message);
    }
    return null;
}

export { createNodeTreeFromXML, genCodeForNode, genCodeForXMLTemplate }
